/**
 *
 */
package br.com.puertorico.model.querybuilder.utils;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import br.com.puertorico.common.utils.ReflectionUtils;
import br.com.puertorico.model.querybuilder.annotations.Argument;
import br.com.puertorico.model.querybuilder.annotations.CriteriaQuery;
import br.com.puertorico.model.querybuilder.annotations.JoinClause;
import br.com.puertorico.model.querybuilder.annotations.JoinClauses;
import br.com.puertorico.model.querybuilder.annotations.WhereClause;
import br.com.puertorico.model.querybuilder.annotations.WhereClauses;
import br.com.puertorico.model.querybuilder.exceptions.InvalidCriteriaQueryException;
import br.com.puertorico.model.querybuilder.exceptions.MissingQueryCountException;
import br.com.puertorico.model.querybuilder.exceptions.MissingQueryException;
import br.com.puertorico.model.querybuilder.handlers.ArgumentHandler;
import br.com.puertorico.model.querybuilder.handlers.JoinClauseHandler;
import br.com.puertorico.model.querybuilder.handlers.WhereClauseHandler;
import br.com.puertorico.model.querybuilder.wrappers.ArgumentWrapper;
import br.com.puertorico.model.querybuilder.wrappers.JoinClauseWrapper;
import br.com.puertorico.model.querybuilder.wrappers.WhereClauseWrapper;

/**
 * @author daniel
 * @version 1.0.0
 */
public class CriteriaReader {

	private final Object criteria;

	private String query;

	private String selectCount;

	private String expressionHint;

	/**
	 * @param criteria
	 */
	public CriteriaReader(final Object criteria) {
		this.criteria = criteria;
		CriteriaQuery cq = criteria.getClass().getAnnotation(
				CriteriaQuery.class);
		validate(cq);
		selectCount = cq.selectCount();
		expressionHint = cq.expressionHint();
	}

	private void validate(CriteriaQuery cq) {
		if (cq == null) {
			throw new MissingQueryException();
		}
		query = cq.query();
		int selectTimes = query.toUpperCase().split("SELECT").length;
		int orderByTimes = query.toUpperCase().split("ORDER\\s*BY").length;
		int groupByTimes = query.toUpperCase().split("GROUP\\s*BY").length;
		int fromTimes = query.toUpperCase().split("FROM").length;
		boolean invalid = false;
		InvalidCriteriaQueryException e = new InvalidCriteriaQueryException();
		if (selectTimes > 2 || fromTimes > 2) {
			invalid = true;
			e.setHasNestedSelect(true);
		}
		if (orderByTimes > 1) {
			invalid = true;
			e.setHasOrderBy(true);
		}
		if (groupByTimes > 1) {
			invalid = true;
			e.setHasGroupBy(true);
		}
		if (invalid) {
			throw e;
		}
	}

	/**
	 * @return a clausula SELECT extraida do objeto criteria
	 */
	public String extractSelectClause() {
		int fromIndex = query.toUpperCase().indexOf("FROM");
		return query.substring(0, fromIndex).trim().replaceAll("\\s+", " ");
	}

	/**
	 * @return a clausula SELECT para o COUNT
	 */
	public String extractSelectCountClause() {
		if (selectCount.equals("")) {
			throw new MissingQueryCountException();
		}
		return selectCount;
	}

	/**
	 * @return a clausula FROM
	 */
	public String extractFromClause() {
		int fromIniIndex = query.toUpperCase().indexOf("FROM");
		int fromEndIndex = query.toUpperCase().indexOf("WHERE");
		return (fromEndIndex == -1) ? (query.substring(fromIniIndex).trim()
				.replaceAll("\\s+", " ")) : (query.substring(fromIniIndex,
				fromEndIndex).trim().replaceAll("\\s+", " "));
	}

	/**
	 * @return um conjunto de argumentos requeridos
	 */
	public Set<ArgumentWrapper> getRequiredArguments() {
		List<ArgumentWrapper> args = extractNotEmptyArguments();
		ArgumentHandler argHandler = new ArgumentHandler(args);
		return argHandler.getRequiredElements();
	}

	/**
	 * @param requiredArguments
	 * @return os joins requeridos
	 */
	public Set<JoinClauseWrapper> getRequiredJoinClauses(
			List<ArgumentWrapper> requiredArguments) {
		JoinClauses joins = criteria.getClass()
				.getAnnotation(JoinClauses.class);
		Set<JoinClauseWrapper> joinSet = new LinkedHashSet<JoinClauseWrapper>();
		if (joins == null) {
			JoinClause join = criteria.getClass().getAnnotation(
					JoinClause.class);
			if (join != null) {
				if (join.alwaysActive()) {
					joinSet.add(new JoinClauseWrapper(join, joinSet.size()));
				} else {
					for (ArgumentWrapper arg : requiredArguments) {
						if (arg.isJoinClauseRequired(join.id())) {
							joinSet.add(new JoinClauseWrapper(join, joinSet
									.size()));
							break;
						}
					}
				}
			}
		} else {
			// recuperar todos os elementos para monta ro grafo
			List<JoinClauseWrapper> elements = new ArrayList<JoinClauseWrapper>();
			List<JoinClauseWrapper> awaysActives = new ArrayList<JoinClauseWrapper>();
			for (JoinClause join : joins.value()) {
				// verificar os requeridos por argumentos
				JoinClauseWrapper wrapper = new JoinClauseWrapper(join,
						joinSet.size());
				if (join.alwaysActive()) {
					awaysActives.add(wrapper);
				}
				elements.add(wrapper);
			}
			// adicionar os que sao sempre ativos.
			joinSet.addAll(awaysActives);
			// vamos buscar os requeridos
			JoinClauseHandler handler = new JoinClauseHandler(elements);
			for (ArgumentWrapper arg : requiredArguments) {
				for (String id : arg.getRequiredJoinClauses()) {
					joinSet.addAll(handler.getRequiredElements(id));
				}
			}
		}
		return joinSet;
	}

	/**
	 * @param requiredArguments
	 * @return os joins requeridos
	 */
	public Set<WhereClauseWrapper> getRequiredWhereClauses(
			List<ArgumentWrapper> requiredArguments) {
		WhereClauses wheres = criteria.getClass().getAnnotation(
				WhereClauses.class);
		Set<WhereClauseWrapper> whereSet = new LinkedHashSet<WhereClauseWrapper>();
		if (wheres == null) {
			WhereClause where = criteria.getClass().getAnnotation(
					WhereClause.class);
			if (where != null) {
				for (ArgumentWrapper arg : requiredArguments) {
					if (where.insertedByDefault()
							|| arg.isWhereClauseRequired(where.id())) {
						WhereClauseWrapper wrapper = new WhereClauseWrapper(
								where, whereSet.size());
						whereSet.add(wrapper);
						break;
					}
				}
			}
		} else {
			// recuperar todos os elementos para monta ro grafo
			List<WhereClauseWrapper> elements = new ArrayList<WhereClauseWrapper>();
			for (WhereClause where : wheres.value()) {
				WhereClauseWrapper wrapper = new WhereClauseWrapper(where,
						elements.size());
				if (where.insertedByDefault()) {
					whereSet.add(wrapper);
				}
				elements.add(wrapper);
			}
			WhereClauseHandler handler = new WhereClauseHandler(elements);
			// verificar os requeridos por argumentos
			for (ArgumentWrapper arg : requiredArguments) {
				for (String id : arg.getRequiredWhereClauses()) {
					whereSet.addAll(handler.getRequiredElements(id));
				}
			}
		}
		return whereSet;
	}

	/**
	 * @return os argumentos nao vazios
	 */
	private List<ArgumentWrapper> extractNotEmptyArguments() {
		List<ArgumentWrapper> args = new ArrayList<ArgumentWrapper>();
		Class<?> superClass = criteria.getClass();
		while (!superClass.equals(Object.class)) {
			for (Field field : superClass.getDeclaredFields()) {
				Argument arg = field.getAnnotation(Argument.class);
				if (arg != null) {
					// extrair o valor e desconsidera se for nulo
					Object valor = ReflectionUtils.extractFieldValue(field,
							criteria);

					// desconsidera valores nulos ou vazios
					if (valor == null || valor.toString().trim().isEmpty()) {
						continue;
					}

					// testa colecoes
					if (valor instanceof Collection<?>) {
						if (((Collection<?>) valor).isEmpty()) {
							continue;
						}
					}
					args.add(new ArgumentWrapper(arg, valor));
				}
			}
			superClass = superClass.getSuperclass();
		}
		return args;
	}

	/**
	 * @param requiredArguments
	 * @return
	 */
	public String getArgumentsWhereClause(
			List<ArgumentWrapper> requiredArguments) {
		StringBuilder where = new StringBuilder();
		if (expressionHint == null || expressionHint.equals("")) {
			boolean first = true;
			for (ArgumentWrapper arg : requiredArguments) {
				if (first) {
					first = false;
					where.append(arg.getWhereClause());
				} else {
					where.append(" AND ").append(arg.getWhereClause());
				}
			}

		} else {
			ArgumentTree tree = new ArgumentTree(requiredArguments,
					expressionHint);
			where.append(tree.getExpression());
		}
		if (where.length() > 0) {
			where.insert(0, "(").append(")");
		}

		return where.toString();
	}

}